﻿using Cognitum.CassandraTableStorage;
using Cognitum.CassandraTableStorage.Entities;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Data.Services.Client;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Configuration;
using System.Web.SessionState;

namespace Cognitum.CassandraProviders.Cassandra
{
    public class CassandraSessionStateProvider : SessionStateStoreProviderBase, IDisposable
    {
        #region | Public Properties |

        public string ApplicationName { get; set; }

        #endregion | Public Properties |

        #region | Public Methods |

        public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
        {
            return new SessionStateStoreData(
                new SessionStateItemCollection(),
                SessionStateUtility.GetSessionStaticObjects(context), timeout
               );
        }

        public override void CreateUninitializedItem(HttpContext context, string partitionKey, int timeout)
        {
            session_state_entity sessionEntity = new session_state_entity()
            {
                session_id = partitionKey,
                application_name = this.ApplicationName,
                expires = DateTimeOffset.Now.AddMinutes((Double)timeout),
                lock_date = DateTimeOffset.Now,
                lock_id = 0,
                time_out = timeout,
                locked = false,
                session_items = String.Empty,
                flags = 1
            };

            CassandraTableStorageDataSource sessionDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            sessionDataSource.Insert(sessionEntity);
        }

        public override void Dispose()
        {
        }

        /// <summary>
        /// Performs any cleanup required by your session-state store provider.
        /// </summary>
        /// <param name="context">The HttpContext instance for the current request</param>
        public override void EndRequest(HttpContext context)
        {
        }

        /// <summary>
        /// This method performs the same work as the GetItemExclusive method, except that it does not attempt to lock the session item in the data store.
        /// </summary>
        /// <param name="context">The HttpContext instance for the current request</param>
        /// <param name="id">The session identifier.</param>
        /// <param name="locked">An output parameter indicating whether the item is currently exclusively locked.</param>
        /// <param name="lockAge">The age of the exclusive lock (if present)</param>
        /// <param name="lockId">The identifier of the exclusive lock (if present)</param>
        /// <param name="actions">Used with sessions whose Cookieless property is true,
        /// when the regenerateExpiredSessionId attribute is set to true.
        /// An actionFlags value set to InitializeItem (1) indicates that the entry in the session data store is a
        /// new session that requires initialization.</param>
        /// <returns>The session data</returns>
        public override SessionStateStoreData GetItem(HttpContext context, string sessionId, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetSessionStoreItem(false, context, sessionId, out locked, out lockAge, out lockId, out actions);
        }

        public override SessionStateStoreData GetItemExclusive(HttpContext context, string sessionId, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetSessionStoreItem(true, context, sessionId, out locked, out lockAge, out lockId, out actions);
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            // Initialize values from web.config.
            if (config == null) throw new ArgumentNullException("config");

            if (String.IsNullOrEmpty(name)) name = "OdbcSessionStateStore";

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Sample ODBC Session State Store provider");
            }

            // Initialize the abstract base class.
            base.Initialize(name, config);

            // Initialize the ApplicationName property.

            ApplicationName = GetConfigValue(ConfigurationManager.AppSettings["ApplicationName"], config["applicationName"]);
            _connectionString = GetConfigValue(ConfigurationManager.AppSettings["ConnectionString"], "localhost");

            // Get <sessionState> configuration element.
            var applicationVirtualPath = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;
            var cfg = WebConfigurationManager.OpenWebConfiguration(applicationVirtualPath);
            pConfig = (SessionStateSection)cfg.GetSection("system.web/sessionState");

            if (!int.TryParse(GetConfigValue(ConfigurationManager.AppSettings["UserIsOnlineTimeWindow"], "15"), out _userIsOnlineTimeWindow)) _userIsOnlineTimeWindow = 15;
            _replicationFactor = Convert.ToInt32(GetConfigValue(ConfigurationManager.AppSettings["ReplicationFactor"], "1"));
        }

        public override void InitializeRequest(HttpContext context)
        {
        }

        public override void ReleaseItemExclusive(HttpContext context, string sessionId, object lockId)
        {
            session_state_entity session = GetSession(sessionId, (int)lockId);
            if (session != null)
            {
                session.locked = false;
                session.expires = DateTimeOffset.Now.AddMinutes(pConfig.Timeout.TotalMinutes);

                CassandraTableStorageDataSource sessionStateDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
                sessionStateDataSource.Update(session);
            }
        }

        public override void RemoveItem(HttpContext context, string sessionId, object lockId, SessionStateStoreData item)
        {
            DeleteSession(sessionId, (int)lockId);
        }

        public override void ResetItemTimeout(HttpContext context, string sessionId)
        {
            session_state_entity session = GetSession(sessionId);
            DateTimeOffset expires = DateTimeOffset.Now.AddMinutes(pConfig.Timeout.TotalMinutes);

            if (session != null) UpdateSessionExpirationDate(session, expires);
        }

        public override void SetAndReleaseItemExclusive(HttpContext context, string sessionId, SessionStateStoreData item, object lockId, bool newItem)
        {
            var sessionItems = Serialize((SessionStateItemCollection)item.Items);
            if (newItem)
            {
                DeleteSession(sessionId, DateTimeOffset.Now);
                CreateSession(sessionId, item.Timeout, sessionItems);
            }
            else
            {
                session_state_entity session = GetSession(sessionId, (int)lockId);
                if (session == null) return;

                session.expires = DateTimeOffset.Now.AddMinutes((Double)item.Timeout);
                session.session_items = sessionItems;
                session.locked = false;

                CassandraTableStorageDataSource sessionDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
                sessionDataSource.Update(session);
            }
        }

        public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
        {
            return false;
        }

        #endregion | Public Methods |

        #region | Private fields |

        private int _replicationFactor;
        private string _connectionString;
        private int _userIsOnlineTimeWindow;
        private SessionStateSection pConfig = null;

        #endregion | Private fields |

        #region | Private Methods |

        /// <summary>
        /// A helper function to retrieve config values from the configuration file
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private static string GetConfigValue(string configValue, string defaultValue)
        {
            string a = String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
            return String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
        }

        private void CreateSession(string sessionId, int timeOut, string sessionItems)
        {
            session_state_entity sessionEntity = new session_state_entity()
            {
                session_id = sessionId,
                application_name = this.ApplicationName,
                expires = DateTimeOffset.Now.AddMinutes((Double)timeOut),
                lock_date = DateTimeOffset.Now,
                lock_id = 0,
                time_out = timeOut,
                locked = false,
                session_items = sessionItems,
                flags = 0
            };
            CassandraTableStorageDataSource sessionDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            try
            {
                sessionDataSource.Insert(sessionEntity);
            }
            catch (DataServiceRequestException)
            {
                // If the table don't exist a DataServiceRequestException is thrown
            }
        }

        private void DeleteSession(string sessionId, int lockId)
        {
            CassandraTableStorageDataSource sessionDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            var session = GetSession(sessionId, lockId);

            if (session != null) sessionDataSource.Delete(session);
        }

        private void DeleteSession(string sessionId)
        {
            CassandraTableStorageDataSource sessionStateDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<session_state_entity> sessionStateEntities = sessionStateDataSource.GetSessions(sessionId, ApplicationName);

            session_state_entity sessionStateEntity = sessionStateEntities.FirstOrDefault();
            if (sessionStateEntity != null)
                sessionStateDataSource.Delete(sessionStateEntity);
        }

        private void DeleteSession(string sessionId, DateTimeOffset expiration)
        {
            CassandraTableStorageDataSource sessionDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            session_state_entity session = GetSession(sessionId, expiration);

            if (session != null) sessionDataSource.Delete(session);
        }

        /// <summary>
        /// DeSerialize is called by the GetSessionStoreItem method to convert the Base64 string stored in
        /// SessionStateEntity::SessionItems property to a SessionStateItemCollection.
        /// </summary>
        /// <param name="context">Current Http context</param>
        /// <param name="serializedItems">Serialized data from SessionStateEntity::SessionItems</param>
        /// <param name="timeout">Milliseconds until SessionStateStoreData times out</param>
        /// <returns></returns>
        private SessionStateStoreData Deserialize(HttpContext context, string serializedItems, int timeout)
        {
            var ms = new MemoryStream(Convert.FromBase64String(serializedItems));

            var sessionItems = new SessionStateItemCollection();

            if (ms.Length > 0)
            {
                var reader = new BinaryReader(ms);
                sessionItems = SessionStateItemCollection.Deserialize(reader);
            }

            return new SessionStateStoreData(sessionItems,
              SessionStateUtility.GetSessionStaticObjects(context),
              timeout);
        }

        private session_state_entity GetSession(string sessionId, int lockId)
        {
            CassandraTableStorageDataSource sessionStateDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<session_state_entity> sessionStateEntities = sessionStateDataSource.GetSessions(sessionId, ApplicationName, lockId);

            return sessionStateEntities.FirstOrDefault();
        }

        private session_state_entity GetSession(string sessionId)
        {
            CassandraTableStorageDataSource sessionStateDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<session_state_entity> sessionStateEntities = sessionStateDataSource.GetSessions(sessionId, ApplicationName);

            try
            {
                return sessionStateEntities.FirstOrDefault();
            }
            catch (DataServiceQueryException)
            {
                // If the table is empty a DataServiceQueryException is thrown
                return null;
            }
        }

        private session_state_entity GetSession(string sessionId, DateTimeOffset expiration)
        {
            CassandraTableStorageDataSource sessionStateDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<session_state_entity> sessionStateEntities = sessionStateDataSource.GetSessions(sessionId, ApplicationName);

            try
            {
                foreach (session_state_entity sessionStateEntity in sessionStateEntities)
                    if (sessionStateEntity.expires < expiration)
                        return sessionStateEntity;
            }
            catch (DataServiceQueryException)
            {
                // If the table don't exist a DataServiceQueryException is thrown
                return null;
            }
            return null;
        }

        private SessionStateStoreData GetSessionStoreItem(bool lockRecord, HttpContext context,
            string sessionId, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags)
        {
            // Initial values for return value and out parameters.
            SessionStateStoreData item = null;
            lockAge = TimeSpan.Zero;
            lockId = null;
            locked = false;
            actionFlags = 0;

            // String to hold serialized SessionStateItemCollection.
            string serializedItems = String.Empty;

            // True if a record is found in the database.
            bool foundRecord = false;

            // True if the returned session item is expired and needs to be deleted.
            bool deleteData = false;

            // Timeout value from the data store.
            int timeout = 0;

            // lockRecord is true when called from GetItemExclusive and false when called from GetItem.
            // Obtain a lock if possible. Ignore the record if it is expired.
            if (lockRecord) locked = LockRecord(sessionId);

            session_state_entity session = GetSession(sessionId);
            if (session != null)
            {
                PrepareExistingSession(ref locked, ref lockAge, ref lockId, ref actionFlags, ref serializedItems, ref foundRecord, ref deleteData, ref timeout, session);
            }
            // If the returned session item is expired then delete the record from the data source.
            if (deleteData) DeleteSession(sessionId);

            // The record was not found. Ensure that locked is false.
            if (!foundRecord) locked = false;

            // If the record was found and you obtained a lock, then set
            // the lockId, clear the actionFlags,
            // and create the SessionStateStoreItem to return.
            if (foundRecord && !locked)
            {
                LockSessionState(ref locked, ref lockId, session);

                item = PrepareSessionStateStoreItem(context, actionFlags, item, serializedItems, timeout);
            }
            return item;
        }

        private void LockSessionState(ref bool locked, ref object lockId, session_state_entity session)
        {
            session.flags = 0;

            lockId = (int)lockId + 1;   // Marked as an "out" parameter
            session.lock_id = (int)lockId;
            locked = session.locked;

            CassandraTableStorageDataSource sessionDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            sessionDataSource.Update(session);
        }

        private SessionStateStoreData PrepareSessionStateStoreItem(HttpContext context, SessionStateActions actionFlags, SessionStateStoreData item, string serializedItems, int timeout)
        {
            // If the actionFlags parameter is not InitializeItem then deserialize the stored SessionStateItemCollection.
            return actionFlags == SessionStateActions.InitializeItem ?
                CreateNewStoreData(context, (int)pConfig.Timeout.TotalMinutes) : Deserialize(context, serializedItems, timeout);
        }

        private void PrepareExistingSession(ref bool locked, ref TimeSpan lockAge, ref object lockId, ref SessionStateActions actionFlags, ref string serializedItems, ref bool foundRecord, ref bool deleteData, ref int timeout, session_state_entity session)
        {
            DateTimeOffset expires = session.expires;

            if (expires < DateTimeOffset.Now)
            {
                // The record was expired. Mark it as not locked.
                locked = false;

                // The session was expired. Mark the data for deletion.
                deleteData = true;
            }
            else foundRecord = true;

            serializedItems = session.session_items;
            lockId = session.lock_id;
            lockAge = DateTimeOffset.Now.Subtract(session.lock_date);
            actionFlags = (SessionStateActions)session.flags;
            timeout = session.time_out;
        }

        private bool LockRecord(string sessionId)
        {
            CassandraTableStorageDataSource sessionStateDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<session_state_entity> sessionStateEntities = sessionStateDataSource.GetSessions(sessionId, ApplicationName, false, DateTimeOffset.Now);

            session_state_entity session;

            try
            {
                session = sessionStateEntities.FirstOrDefault();
            }
            catch (DataServiceQueryException)
            {
                // If the table is empty a DataServiceQueryException is thrown
                return true;
            }

            if (session == null) return true;

            UpdateSessionLockedState(session, true);

            return false;
        }

        /// <summary>
        /// Serialize is called by the SetAndReleaseItemExclusive method to
        /// convert the SessionStateItemCollection into a Base64 string to
        /// be stored in an Access Memo field.
        /// </summary>
        /// <param name="items"></param>
        /// <returns></returns>
        private string Serialize(SessionStateItemCollection items)
        {
            MemoryStream ms = new MemoryStream();
            BinaryWriter writer = new BinaryWriter(ms);

            if (items != null) items.Serialize(writer);

            writer.Close();

            return Convert.ToBase64String(ms.ToArray());
        }

        private void UpdateSessionExpirationDate(session_state_entity session, DateTimeOffset expires)
        {
            CassandraTableStorageDataSource sessionDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            session.expires = expires;
            try
            {
                sessionDataSource.Update(session);
            }
            catch (DataServiceRequestException)
            {
                // A DataServiceClientException is sometimes thrown here when starting up the solution
            }
            catch (DataServiceClientException)
            {
                // A DataServiceClientException is sometimes thrown here when starting up the solution
            }
        }

        private void UpdateSessionLockedState(session_state_entity session, bool locked)
        {
            session.locked = locked;

            if (locked) session.lock_date = DateTimeOffset.Now;

            CassandraTableStorageDataSource sessionStateDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            sessionStateDataSource.Update(session);
        }

        #endregion | Private Methods |
    }
}